package com.sinnatec.redis.lock.impl;

import io.netty.util.Timeout;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import com.sinnatec.redis.lock.LockTimer;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName ReentrantLock
 * @Description redis实现可重入锁
 * @Author fangsong
 * @Date 2021/12/6 16:07
 * @Version 1.0
 */
public class ReentrantLock extends AbstractRedisLock {

    /**
     * 加锁lua脚本
     */
    private static final String LOCK_SCRIPT = "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);";

    /**
     * 释放锁lua脚本
     */
    private static final String RELEASE_SCRIPT = "if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[1]); return 0; else redis.call('del', KEYS[1]); return 1; end; return nil;";

    /**
     * 看门狗时间刷新lua脚本
     */
    private static final String RENEW_SCRIPT = "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]);return 1;end;return 0;";


    public ReentrantLock(StringRedisTemplate redisTemplate, String lockName) {
        super(redisTemplate, lockName);
    }

    /**
     * 加锁，直接返回成功或者失败
     * @return
     *              是否加锁成功
     */
    public boolean getLock(){
        Long pttl = executeScript(LOCK_SCRIPT,getLockName(),getLeaseTime(),getTransId());
        //如果返回空代表加锁成功
        if(pttl == null){
            startWatchDog();
            return true;
        }
        return false;
    }

    /**
     *
     * 尝试加锁，使用默认超时时长
     *
     * @return
     *              是否加锁成功
     */
    public boolean tryLock() {
        return tryLock(getWaitTime());
    }

    /**
     * 尝试加锁
     *
     * @param timeout
     *                  超时时长，如果超过此时间没有获取锁则返回false，单位毫秒，小于0代表不会超时，一直等待
     *
     * @return      是否加锁成功
     */
    public boolean tryLock(long timeout) {
        long beforeGetLockMillis = System.currentTimeMillis();
        //RedisScript<Long> script = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
        //StringRedisTemplate redisTemplate = getRedisTemplate();
        //Long pttl = redisTemplate.execute(script, Collections.singletonList(getLockName()), String.valueOf(getLeaseTime()), getTransId());
        Long pttl = executeScript(LOCK_SCRIPT,getLockName(),getLeaseTime(),getTransId());
        //如果返回空代表加锁成功
        if(pttl==null){
            startWatchDog();
            return true;
        }
        //-2代表key不存在，-1代表key永不过期，这两种情况都无法成功获取锁
        if(pttl<0){
            return false;
        }

        //计算获取一次锁失败后的剩余超时时长
        long remainTimeout = timeout-(System.currentTimeMillis()-beforeGetLockMillis);

        //锁可能提前释放，不会等到pttl到0，所以此处判断取消
        //如果设置了超时时长并且key剩余过期时间大于超时时间，超时前无法成功获取锁，直接失败
        /*if(timeout>=0&&pttl>remainTimeout){
            return false;
        }*/

        //自旋获取锁，直到超时，如果超时时长为负值，则一直循环盲等
        while(timeout<0||remainTimeout>=0){
            //pttl = redisTemplate.execute(script, Collections.singletonList(getLockName()), String.valueOf(getLeaseTime()), getTransId());
            pttl = executeScript(LOCK_SCRIPT,getLockName(),getLeaseTime(),getTransId());
            //如果返回空代表加锁成功
            if(pttl==null){
                startWatchDog();
                return true;
            }
            long currentMillis = System.currentTimeMillis();
            remainTimeout = timeout-(currentMillis-beforeGetLockMillis);
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @return
     *              是否释放成功
     */
    @Override
    public boolean releaseLock() {
        cancelWatchDog();
        Long result = executeScript(RELEASE_SCRIPT,getLockName(),getLeaseTime(),getTransId());
        if(result==null){
            return false;
        }
        return result == 0 || result == 1;
    }

    /**
     * 对锁进行续期
     * @return
     *              续期是否成功
     */
    protected boolean renewalLock(){
        Long result = executeScript(RENEW_SCRIPT,getLockName(),getLeaseTime(),getTransId());
        if(result==null){
            return false;
        }
        return result == 1;
    }

    /**
     * 执行lua脚本
     * @param script
     *                      脚本
     * @param lockName
     *                      锁名称
     * @param leaseTime
     *                      锁持有时间
     * @param transId
     *                      锁唯一标识
     * @return
     *                      执行结果
     */
    private Long executeScript(String script,String lockName,long leaseTime,String transId){
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        StringRedisTemplate redisTemplate = getRedisTemplate();
        return redisTemplate.execute(redisScript, Collections.singletonList(lockName), String.valueOf(leaseTime), transId);
    }

    /**
     * 续期后再次订阅自己进行续期
     */
    private synchronized void renewExpiration(){
        renewalLock();
        LockTimer lockTimer = LockTimer.getLockTimer();
        LockTimer.ExpirationEntry entry = lockTimer.getEntry(getLockName());
        if(entry==null){
            return;
        }

        Timeout task = lockTimer.newTimeout(
                timeout -> {
                    renewExpiration();
                }, getLeaseTime() / 3, TimeUnit.MILLISECONDS
        );
        entry.setTimeout(task);
    }

    /**
     * 启动看门狗，key到期续期
     */
    private synchronized void startWatchDog(){
        LockTimer lockTimer = LockTimer.getLockTimer();
        LockTimer.ExpirationEntry entry = new LockTimer.ExpirationEntry();
        LockTimer.ExpirationEntry oldEntry = lockTimer.putEntryIfAbsent(getLockName(),entry);
        if(oldEntry!=null){
            oldEntry.increaseCount();
            return;
        }
        Timeout task = lockTimer.newTimeout(
                timeout -> {
                    renewExpiration();
                }, getLeaseTime() / 3, TimeUnit.MILLISECONDS
        );
        entry.increaseCount();
        entry.setTimeout(task);
    }

    /**
     * 释放锁时停止看门狗
     */
    private synchronized void cancelWatchDog(){
        LockTimer lockTimer = LockTimer.getLockTimer();
        LockTimer.ExpirationEntry entry = lockTimer.getEntry(getLockName());
        if(entry==null){
            return;
        }
        int count = entry.decrementCount();
        if(count==0){
            entry.getTimeout().cancel();
            lockTimer.removeEntry(getLockName());
        }
    }

}
